]> dgit.raspbian.org Git - gnome-shell.git/commitdiff
extensions: Refactor extension system for asynchronous loading
authorEvan Welsh <contact@evanwelsh.com>
Thu, 24 Nov 2022 19:55:18 +0000 (13:55 -0600)
committerMarge Bot <marge-bot@gnome.org>
Thu, 1 Dec 2022 12:59:32 +0000 (12:59 +0000)
This commit refactors extensionSystem to load extensions
asynchronously. This will enable loading extensions using ECMAScript
modules in a future commit.

Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2364>

js/ui/extensionSystem.js

index 5dfb325f2d3f9d9632df7a9968d7a817b7895d08..4cdaa3dad9c1e40c2762d7db5b12c8737823e5cd 100644 (file)
@@ -23,7 +23,7 @@ var ExtensionManager = class extends Signals.EventEmitter {
     constructor() {
         super();
 
-        this._initialized = false;
+        this._initializationPromise = null;
         this._updateNotified = false;
 
         this._extensions = new Map();
@@ -32,7 +32,9 @@ var ExtensionManager = class extends Signals.EventEmitter {
         this._extensionOrder = [];
         this._checkVersion = false;
 
-        Main.sessionMode.connect('updated', this._sessionUpdated.bind(this));
+        Main.sessionMode.connect('updated', () => {
+            this._sessionUpdated();
+        });
     }
 
     init() {
@@ -54,13 +56,15 @@ var ExtensionManager = class extends Signals.EventEmitter {
         });
 
         this._installExtensionUpdates();
-        this._sessionUpdated();
+        this._sessionUpdated().then(() => {
+            ExtensionDownloader.checkForUpdates();
+        });
 
         GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, UPDATE_CHECK_TIMEOUT, () => {
             ExtensionDownloader.checkForUpdates();
+
             return GLib.SOURCE_CONTINUE;
         });
-        ExtensionDownloader.checkForUpdates();
     }
 
     get updatesSupported() {
@@ -92,12 +96,12 @@ var ExtensionManager = class extends Signals.EventEmitter {
         return false;
     }
 
-    _callExtensionDisable(uuid) {
+    async _callExtensionDisable(uuid) {
         let extension = this.lookup(uuid);
         if (!extension)
             return;
 
-        if (extension.state != ExtensionState.ENABLED)
+        if (extension.state !== ExtensionState.ENABLED)
             return;
 
         extension.state = ExtensionState.DISABLING;
@@ -139,7 +143,8 @@ var ExtensionManager = class extends Signals.EventEmitter {
         for (let i = 0; i < order.length; i++) {
             let otherUuid = order[i];
             try {
-                this.lookup(otherUuid).stateObj.enable();
+                // eslint-disable-next-line no-await-in-loop
+                await this.lookup(otherUuid).stateObj.enable();
             } catch (e) {
                 this.logExtensionError(otherUuid, e);
             }
@@ -147,13 +152,13 @@ var ExtensionManager = class extends Signals.EventEmitter {
 
         this._extensionOrder.splice(orderIdx, 1);
 
-        if (extension.state != ExtensionState.ERROR) {
+        if (extension.state !== ExtensionState.ERROR) {
             extension.state = ExtensionState.DISABLED;
             this.emit('extension-state-changed', extension);
         }
     }
 
-    _callExtensionEnable(uuid) {
+    async _callExtensionEnable(uuid) {
         if (!this._extensionSupportsSessionMode(uuid))
             return;
 
@@ -161,10 +166,11 @@ var ExtensionManager = class extends Signals.EventEmitter {
         if (!extension)
             return;
 
-        if (extension.state == ExtensionState.INITIALIZED)
-            this._callExtensionInit(uuid);
+        if (extension.state === ExtensionState.INITIALIZED)
+            await this._callExtensionInit(uuid);
 
-        if (extension.state != ExtensionState.DISABLED)
+
+        if (extension.state !== ExtensionState.DISABLED)
             return;
 
         extension.state = ExtensionState.ENABLING;
@@ -187,7 +193,7 @@ var ExtensionManager = class extends Signals.EventEmitter {
         }
 
         try {
-            extension.stateObj.enable();
+            await extension.stateObj.enable();
             extension.state = ExtensionState.ENABLED;
             this._extensionOrder.push(uuid);
             this.emit('extension-state-changed', extension);
@@ -372,7 +378,7 @@ var ExtensionManager = class extends Signals.EventEmitter {
         return extension.metadata.version === version;
     }
 
-    loadExtension(extension) {
+    async loadExtension(extension) {
         // Default to error, we set success as the last step
         extension.state = ExtensionState.ERROR;
 
@@ -385,10 +391,11 @@ var ExtensionManager = class extends Signals.EventEmitter {
             let enabled = this._enabledExtensions.includes(extension.uuid) &&
                           this._extensionSupportsSessionMode(extension.uuid);
             if (enabled) {
-                if (!this._callExtensionInit(extension.uuid))
+                if (!await this._callExtensionInit(extension.uuid))
                     return;
-                if (extension.state == ExtensionState.DISABLED)
-                    this._callExtensionEnable(extension.uuid);
+
+                if (extension.state === ExtensionState.DISABLED)
+                    await this._callExtensionEnable(extension.uuid);
             } else {
                 extension.state = ExtensionState.INITIALIZED;
             }
@@ -400,27 +407,26 @@ var ExtensionManager = class extends Signals.EventEmitter {
         this.emit('extension-state-changed', extension);
     }
 
-    unloadExtension(extension) {
+    async unloadExtension(extension) {
         const { uuid, type } = extension;
 
         // Try to disable it -- if it's ERROR'd, we can't guarantee that,
         // but it will be removed on next reboot, and hopefully nothing
         // broke too much.
-        this._callExtensionDisable(uuid);
+        await this._callExtensionDisable(uuid);
 
         extension.state = ExtensionState.UNINSTALLED;
         this.emit('extension-state-changed', extension);
 
-        // If we did install an importer, it is now cached and it's
-        // impossible to load a different version
-        if (type === ExtensionType.PER_USER && extension.imports)
+        // The extension is now cached and it's impossible to load a different version
+        if (type === ExtensionType.PER_USER && extension.isImported)
             this._unloadedExtensions.set(uuid, extension.metadata.version);
 
         this._extensions.delete(uuid);
         return true;
     }
 
-    reloadExtension(oldExtension) {
+    async reloadExtension(oldExtension) {
         // Grab the things we'll need to pass to createExtensionObject
         // to reload it.
         let { uuid, dir, type } = oldExtension;
@@ -437,10 +443,10 @@ var ExtensionManager = class extends Signals.EventEmitter {
             return;
         }
 
-        this.loadExtension(newExtension);
+        await this.loadExtension(newExtension);
     }
 
-    _callExtensionInit(uuid) {
+    async _callExtensionInit(uuid) {
         if (!this._extensionSupportsSessionMode(uuid))
             return false;
 
@@ -458,7 +464,16 @@ var ExtensionManager = class extends Signals.EventEmitter {
         let extensionModule;
         let extensionState = null;
 
+        // TODO: This function does not have an async operation but when ESM is
+        // merged there will be a dynamic import here instead of `installImporter`.
+        await 0;
+
         ExtensionUtils.installImporter(extension);
+
+        // Extensions can only be imported once, so add a property to avoid
+        // attempting to re-import an extension.
+        extension.isImported = true;
+
         try {
             extensionModule = extension.imports.extension;
         } catch (e) {
@@ -468,7 +483,7 @@ var ExtensionManager = class extends Signals.EventEmitter {
 
         if (extensionModule.init) {
             try {
-                extensionState = extensionModule.init(extension);
+                extensionState = await extensionModule.init(extension);
             } catch (e) {
                 this.logExtensionError(uuid, e);
                 return false;
@@ -492,8 +507,8 @@ var ExtensionManager = class extends Signals.EventEmitter {
 
     _updateCanChange(extension) {
         let hasError =
-            extension.state == ExtensionState.ERROR ||
-            extension.state == ExtensionState.OUT_OF_DATE;
+            extension.state === ExtensionState.ERROR ||
+            extension.state === ExtensionState.OUT_OF_DATE;
 
         let isMode = this._getModeExtensions().includes(extension.uuid);
         let modeOnly = global.settings.get_boolean(DISABLE_USER_EXTENSIONS_KEY);
@@ -519,27 +534,36 @@ var ExtensionManager = class extends Signals.EventEmitter {
         return extensions.filter(item => !disabledExtensions.includes(item));
     }
 
-    _onUserExtensionsEnabledChanged() {
-        this._onEnabledExtensionsChanged();
+    async _onUserExtensionsEnabledChanged() {
+        await this._onEnabledExtensionsChanged();
         this._onSettingsWritableChanged();
     }
 
-    _onEnabledExtensionsChanged() {
+    async _onEnabledExtensionsChanged() {
         let newEnabledExtensions = this._getEnabledExtensions();
 
         // Find and enable all the newly enabled extensions: UUIDs found in the
         // new setting, but not in the old one.
-        newEnabledExtensions
+        const extensionsToEnable = newEnabledExtensions
             .filter(uuid => !this._enabledExtensions.includes(uuid) &&
-                             this._extensionSupportsSessionMode(uuid))
-            .forEach(uuid => this._callExtensionEnable(uuid));
+                             this._extensionSupportsSessionMode(uuid));
+        for (const uuid of extensionsToEnable) {
+            // eslint-disable-next-line no-await-in-loop
+            await this._callExtensionEnable(uuid);
+        }
 
         // Find and disable all the newly disabled extensions: UUIDs found in the
         // old setting, but not in the new one.
-        this._extensionOrder
+        const extensionsToDisable = this._extensionOrder
             .filter(uuid => !newEnabledExtensions.includes(uuid) ||
-                            !this._extensionSupportsSessionMode(uuid))
-            .reverse().forEach(uuid => this._callExtensionDisable(uuid));
+                            !this._extensionSupportsSessionMode(uuid));
+        // Reverse mutates the original array, but .filter() creates a new array.
+        extensionsToDisable.reverse();
+
+        for (const uuid of extensionsToDisable) {
+            // eslint-disable-next-line no-await-in-loop
+            await this._callExtensionDisable(uuid);
+        }
 
         this._enabledExtensions = newEnabledExtensions;
     }
@@ -551,7 +575,7 @@ var ExtensionManager = class extends Signals.EventEmitter {
         }
     }
 
-    _onVersionValidationChanged() {
+    async _onVersionValidationChanged() {
         const checkVersion = !global.settings.get_boolean(EXTENSION_DISABLE_VERSION_CHECK_KEY);
         if (checkVersion === this._checkVersion)
             return;
@@ -561,16 +585,18 @@ var ExtensionManager = class extends Signals.EventEmitter {
         // Disabling extensions modifies the order array, so use a copy
         let extensionOrder = this._extensionOrder.slice();
 
-        // Disable enabled extensions in the reverse order first to avoid
+        // Disable enabled extensions first to avoid
         // the "rebasing" done in _callExtensionDisable...
-        extensionOrder.slice().reverse().forEach(uuid => {
-            this._callExtensionDisable(uuid);
-        });
+        this._disableAllExtensions();
 
         // ...and then reload and enable extensions in the correct order again.
-        [...this._extensions.values()].sort((a, b) => {
+        const extensionsToReload = [...this._extensions.values()].sort((a, b) => {
             return extensionOrder.indexOf(a.uuid) - extensionOrder.indexOf(b.uuid);
-        }).forEach(extension => this.reloadExtension(extension));
+        });
+        for (const extension of extensionsToReload) {
+            // eslint-disable-next-line no-await-in-loop
+            await this.reloadExtension(extension);
+        }
     }
 
     _installExtensionUpdates() {
@@ -596,21 +622,25 @@ var ExtensionManager = class extends Signals.EventEmitter {
         }
     }
 
-    _loadExtensions() {
-        global.settings.connect(`changed::${ENABLED_EXTENSIONS_KEY}`,
-            this._onEnabledExtensionsChanged.bind(this));
-        global.settings.connect(`changed::${DISABLED_EXTENSIONS_KEY}`,
-            this._onEnabledExtensionsChanged.bind(this));
-        global.settings.connect(`changed::${DISABLE_USER_EXTENSIONS_KEY}`,
-            this._onUserExtensionsEnabledChanged.bind(this));
-        global.settings.connect(`changed::${EXTENSION_DISABLE_VERSION_CHECK_KEY}`,
-            this._onVersionValidationChanged.bind(this));
-        global.settings.connect(`writable-changed::${ENABLED_EXTENSIONS_KEY}`,
-            this._onSettingsWritableChanged.bind(this));
-        global.settings.connect(`writable-changed::${DISABLED_EXTENSIONS_KEY}`,
-            this._onSettingsWritableChanged.bind(this));
+    async _loadExtensions() {
+        global.settings.connect(`changed::${ENABLED_EXTENSIONS_KEY}`, () => {
+            this._onEnabledExtensionsChanged();
+        });
+        global.settings.connect(`changed::${DISABLED_EXTENSIONS_KEY}`, () => {
+            this._onEnabledExtensionsChanged();
+        });
+        global.settings.connect(`changed::${DISABLE_USER_EXTENSIONS_KEY}`, () => {
+            this._onUserExtensionsEnabledChanged();
+        });
+        global.settings.connect(`changed::${EXTENSION_DISABLE_VERSION_CHECK_KEY}`, () => {
+            this._onVersionValidationChanged();
+        });
+        global.settings.connect(`writable-changed::${ENABLED_EXTENSIONS_KEY}`, () =>
+            this._onSettingsWritableChanged());
+        global.settings.connect(`writable-changed::${DISABLED_EXTENSIONS_KEY}`, () =>
+            this._onSettingsWritableChanged());
 
-        this._onVersionValidationChanged();
+        await this._onVersionValidationChanged();
 
         this._enabledExtensions = this._getEnabledExtensions();
 
@@ -642,33 +672,50 @@ var ExtensionManager = class extends Signals.EventEmitter {
             return extension;
         }).filter(extension => extension !== null);
 
-        for (const extension of extensionObjects)
-            this.loadExtension(extension);
+        for (const extension of extensionObjects) {
+            // eslint-disable-next-line no-await-in-loop
+            await this.loadExtension(extension);
+        }
     }
 
-    _enableAllExtensions() {
-        if (!this._initialized) {
-            this._loadExtensions();
-            this._initialized = true;
-        } else {
-            this._enabledExtensions.forEach(uuid => {
-                this._callExtensionEnable(uuid);
-            });
+    async _enableAllExtensions() {
+        if (!this._initializationPromise)
+            this._initializationPromise = this._loadExtensions();
+
+        await this._initializationPromise;
+
+        for (const uuid of this._enabledExtensions) {
+            // eslint-disable-next-line no-await-in-loop
+            await this._callExtensionEnable(uuid);
         }
     }
 
-    _disableAllExtensions() {
-        if (this._initialized) {
-            this._extensionOrder.slice().reverse().forEach(uuid => {
-                this._callExtensionDisable(uuid);
-            });
+
+    /**
+     * Disables all currently enabled extensions.
+     */
+    async _disableAllExtensions() {
+        // Wait for extensions to finish loading before starting
+        // to disable, otherwise some extensions may enable after
+        // this function.
+        if (this._initializationPromise)
+            await this._initializationPromise;
+
+        const extensionsToDisable = this._extensionOrder.slice();
+        // Extensions are disabled in the reverse order
+        // from when they were enabled.
+        extensionsToDisable.reverse();
+
+        for (const uuid of extensionsToDisable) {
+            // eslint-disable-next-line no-await-in-loop
+            await this._callExtensionDisable(uuid);
         }
     }
 
-    _sessionUpdated() {
+    async _sessionUpdated() {
         // Take care of added or removed sessionMode extensions
-        this._onEnabledExtensionsChanged();
-        this._enableAllExtensions();
+        await this._onEnabledExtensionsChanged();
+        await this._enableAllExtensions();
     }
 };